package com.fsgom.common.xml;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.transform.Source;

import org.springframework.xml.transform.StaxSource;

import com.fsgom.common.util.Generics;

@SuppressWarnings("deprecation")
public final class XmlUtils {
	private static final XMLInputFactory XML_INPUT_FACTORY = XMLInputFactory.newInstance();
	private static final Map<String, JAXBContext> JAXB_CONTEXT = new ConcurrentHashMap<String, JAXBContext>();

	static {
		XML_INPUT_FACTORY.setProperty("javax.xml.stream.isValidating", Boolean.FALSE);
		XML_INPUT_FACTORY.setProperty("javax.xml.stream.isCoalescing", Boolean.TRUE);
		XML_INPUT_FACTORY.setProperty("javax.xml.stream.isNamespaceAware", Boolean.FALSE);
	}

	/**
	 * 생성자 (호출되지 않음)
	 */
	private XmlUtils() {
		throw new UnsupportedOperationException();
	}

	/**
	 * {@link JaxbContext} 를 가져온다. (없는 경우 등록)
	 * 
	 * @param classes
	 * @return
	 * @throws JAXBException
	 */
	private static JAXBContext getJaxbContext(Class<?>... classes) throws JAXBException {
		String key = Arrays.toString(classes);

		if (JAXB_CONTEXT.containsKey(key)) {
			return JAXB_CONTEXT.get(key);
		} else {
			JAXBContext jaxbContext = JAXBContext.newInstance(classes);
			JAXB_CONTEXT.put(key, jaxbContext);
			return jaxbContext;
		}
	}

	/**
	 * XML 문자열을 생성하기 위한 메소드
	 * 
	 * @param text 문자열
	 * @return XML 문자열
	 */
	public static String forXml(String text) {
		final StringBuffer sb = new StringBuffer();
		final StringCharacterIterator iterator = new StringCharacterIterator(text);
		char character = iterator.current();

		while (character != CharacterIterator.DONE) {
			if (character == '<') {
				sb.append("&lt;");
			} else if (character == '>') {
				sb.append("&gt;");
			} else if (character == '\"') {
				sb.append("&quot;");
			} else if (character == '\'') {
				sb.append("&#039;");
			} else if (character == '&') {
				sb.append("&amp;");
			} else {
				sb.append(character);
			}

			character = iterator.next();
		}

		return sb.toString();
	}

	/**
	 * {@link Properties}에 forXml 메소드를 적용
	 * 
	 * @param properties {@link Properties}
	 * @return XML 문자열 변환된 {@link Properties}
	 */
	public static Properties forXml(Properties properties) {
		Properties props = new Properties();

		for (Entry<Object, Object> entry : properties.entrySet()) {
			if (entry.getValue() instanceof String) {
				props.put(entry.getKey(), forXml((String) entry.getValue()));
			} else {
				props.put(entry.getKey(), entry.getValue());
			}
		}

		return props;
	}

	/**
	 * {@link Map}에 forXml 메소드를 적용
	 * 
	 * @param map {@link Map}
	 * @return XML 문자열 변환된 {@link Map}
	 */
	public static Map<String, Object> forXml(Map<String, Object> map) {
		Map<String, Object> ma = new LinkedHashMap<String, Object>();

		for (Entry<String, Object> entry : map.entrySet()) {
			if (entry.getValue() instanceof String) {
				ma.put(entry.getKey(), forXml((String) entry.getValue()));
			} else {
				ma.put(entry.getKey(), entry.getValue());
			}
		}

		return ma;
	}

	public static String marshall(Object object, String encoding, Class<?>... classes) throws IOException {
		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

		try {
			JAXBContext jaxbContext = getJaxbContext(getClassesToBeBound(object.getClass(), classes));
			Marshaller marshaller = jaxbContext.createMarshaller();

			marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
			//marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
			marshaller.setProperty(Marshaller.JAXB_ENCODING, encoding);

			marshaller.marshal(object, outputStream);
			return outputStream.toString(encoding);
		} catch (JAXBException e) {
			throw new IOException(e);
		} catch (UnsupportedEncodingException e) {
			throw new IOException(e);
		}
	}

	private static Class<?>[] getClassesToBeBound(Class<?> clazz, Class<?>... classes) {
		List<Class<?>> classesToBeBound = Generics.list();
		classesToBeBound.add(clazz);

		if (classes != null) {
			for (Class<?> cls : classes) {
				classesToBeBound.add(cls);
			}
		}

		return classesToBeBound.toArray(new Class<?>[classesToBeBound.size()]);
	}

	@SuppressWarnings("unchecked")
	public static <T> T unmarshall(InputStream inputStream, Class<T> clazz, Class<?>... classes) throws IOException {
		try {
			Source source = new StaxSource(XML_INPUT_FACTORY.createXMLStreamReader(inputStream));
			JAXBContext jaxbContext = getJaxbContext(getClassesToBeBound(clazz, classes));
			Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();

			return (T) unmarshaller.unmarshal(source);
		} catch (XMLStreamException e) {
			throw new IOException(e);
		} catch (JAXBException e) {
			throw new IOException(e);
		}
	}

	public static <T> JAXBElement<T> unmarshallElement(InputStream inputStream, Class<T> clazz, Class<?>... classes) throws IOException {
		try {
			Source source = new StaxSource(XML_INPUT_FACTORY.createXMLStreamReader(inputStream));
			JAXBContext jaxbContext = getJaxbContext(getClassesToBeBound(clazz, classes));
			Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();

			return unmarshaller.unmarshal(source, clazz);
		} catch (XMLStreamException e) {
			throw new IOException(e);
		} catch (JAXBException e) {
			throw new IOException(e);
		}
	}
}
